@abraca/dabra 1.8.2 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10456,6 +10456,15 @@ var FileBlobStore = class FileBlobStore extends EventEmitter {
10456
10456
  this.objectUrls.delete(key);
10457
10457
  }
10458
10458
  }
10459
+ /**
10460
+ * Clear the 404 negative-cache entry for (docId, uploadId) so the next
10461
+ * getBlobUrl() re-fetches from the server instead of short-circuiting.
10462
+ * Use on explicit user retry or after reconnect, when the file may have
10463
+ * become available since the last 404.
10464
+ */
10465
+ clearNotFound(docId, uploadId) {
10466
+ this._notFound.delete(this.blobKey(docId, uploadId));
10467
+ }
10459
10468
  /** Revoke the object URL and remove the blob from cache. */
10460
10469
  async evictBlob(docId, uploadId) {
10461
10470
  const key = this.blobKey(docId, uploadId);
@@ -10856,6 +10865,19 @@ var E2EOfflineStore = class extends OfflineStore {
10856
10865
  * are fetched on subsequent connects.
10857
10866
  * - After sync, a fresh encrypted snapshot is saved.
10858
10867
  *
10868
+ * Client-side compaction:
10869
+ * - After `compactionThreshold` encrypted updates have been applied in this
10870
+ * session (local + remote), and the doc has been quiescent for
10871
+ * `compactionQuiescenceMs`, the provider merges the whole Y.Doc, encrypts it,
10872
+ * and sends `snapshot:compact` — the server atomically replaces the per-doc
10873
+ * update log with that single compacted blob. The server acknowledges by
10874
+ * broadcasting `snapshot:compacted`, which emits the `"compacted"` event.
10875
+ * - Requires Owner or above (server silently drops non-Owner requests).
10876
+ * - Callers that want a final compaction before teardown should
10877
+ * `await provider.compactNow()` before `destroy()`. `destroy()` does not
10878
+ * compact (it'd race with the socket teardown) — any pending debounce is
10879
+ * cancelled.
10880
+ *
10859
10881
  * Key availability limitation: if the user's WebAuthn key is not in
10860
10882
  * DocKeyManager's in-memory cache and there is no network, E2E docs show
10861
10883
  * empty — the key fetch requires either a cached in-memory key or network.
@@ -10863,6 +10885,11 @@ var E2EOfflineStore = class extends OfflineStore {
10863
10885
  function fromBase64(b64) {
10864
10886
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
10865
10887
  }
10888
+ function toBase64(bytes) {
10889
+ let bin = "";
10890
+ for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
10891
+ return btoa(bin);
10892
+ }
10866
10893
  var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraProvider {
10867
10894
  constructor(configuration) {
10868
10895
  super({
@@ -10872,9 +10899,17 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10872
10899
  this.docKey = null;
10873
10900
  this.lastSeq = -1;
10874
10901
  this.e2eStore = null;
10902
+ this.updatesSinceCompaction = 0;
10903
+ this.compactionInFlight = false;
10904
+ this.compactionInFlightTimeout = null;
10905
+ this.compactionDebounceTimer = null;
10906
+ this.destroyed = false;
10875
10907
  this.docKeyManager = configuration.docKeyManager;
10876
10908
  this.keystore = configuration.keystore;
10877
10909
  this.e2eClient = configuration.client;
10910
+ this.compactionEnabled = configuration.compactionEnabled !== false;
10911
+ this.compactionThreshold = Math.max(1, configuration.compactionThreshold ?? 50);
10912
+ this.compactionQuiescenceMs = Math.max(0, configuration.compactionQuiescenceMs ?? 2e3);
10878
10913
  this.e2eServerOrigin = E2EAbracadabraProvider.deriveServerOrigin(configuration, configuration.client);
10879
10914
  }
10880
10915
  /** Fetch the doc key from the server (requires WebAuthn if not cached). */
@@ -10886,6 +10921,10 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10886
10921
  }
10887
10922
  /** Handle stateless messages including e2e_ready and e2e_update. */
10888
10923
  receiveStateless(payload) {
10924
+ if (payload.startsWith("snapshot:compacted ")) {
10925
+ this._handleCompactedBroadcast(payload.slice(19));
10926
+ return;
10927
+ }
10889
10928
  let parsed;
10890
10929
  try {
10891
10930
  parsed = JSON.parse(payload);
@@ -10937,6 +10976,7 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10937
10976
  const plaintext = await decryptField(encryptedData, key);
10938
10977
  yjs.applyUpdate(this.document, plaintext, this);
10939
10978
  this.lastSeq = Math.max(this.lastSeq, seq);
10979
+ this._noteUpdateApplied();
10940
10980
  } catch (e) {
10941
10981
  console.error("[E2EAbracadabraProvider] decryption failed for seq", seq, e);
10942
10982
  }
@@ -10959,8 +10999,93 @@ var E2EAbracadabraProvider = class E2EAbracadabraProvider extends AbracadabraPro
10959
10999
  update: encrypted,
10960
11000
  documentName: this.configuration.name
10961
11001
  });
11002
+ this._noteUpdateApplied();
11003
+ }
11004
+ /**
11005
+ * Force an immediate compaction attempt, bypassing the threshold and
11006
+ * quiescence debounce. Resolves once the `snapshot:compact` frame has been
11007
+ * sent (or rejected locally for missing prerequisites — destroyed, no
11008
+ * doc key, or already in-flight). The server acknowledges via a
11009
+ * `snapshot:compacted` broadcast, which emits the `"compacted"` event.
11010
+ */
11011
+ async compactNow() {
11012
+ if (this.compactionDebounceTimer) {
11013
+ clearTimeout(this.compactionDebounceTimer);
11014
+ this.compactionDebounceTimer = null;
11015
+ }
11016
+ await this._performCompaction();
11017
+ }
11018
+ _noteUpdateApplied() {
11019
+ if (!this.compactionEnabled || this.destroyed) return;
11020
+ this.updatesSinceCompaction += 1;
11021
+ if (this.updatesSinceCompaction < this.compactionThreshold) return;
11022
+ if (this.compactionDebounceTimer) clearTimeout(this.compactionDebounceTimer);
11023
+ this.compactionDebounceTimer = setTimeout(() => {
11024
+ this.compactionDebounceTimer = null;
11025
+ this._performCompaction().catch((e) => {
11026
+ console.error("[E2EAbracadabraProvider] compaction failed:", e);
11027
+ });
11028
+ }, this.compactionQuiescenceMs);
11029
+ }
11030
+ async _performCompaction() {
11031
+ if (this.destroyed) return;
11032
+ if (this.compactionInFlight) return;
11033
+ if (this.updatesSinceCompaction === 0) return;
11034
+ if (!this.synced) return;
11035
+ const key = await this.ensureDocKey();
11036
+ if (!key) return;
11037
+ if (this.destroyed) return;
11038
+ this.compactionInFlight = true;
11039
+ try {
11040
+ const stateVector = yjs.encodeStateVector(this.document);
11041
+ const encrypted = await encryptField(yjs.encodeStateAsUpdate(this.document), key);
11042
+ if (this.destroyed) {
11043
+ this.compactionInFlight = false;
11044
+ return;
11045
+ }
11046
+ const payload = `snapshot:compact ${JSON.stringify({
11047
+ state_vector: toBase64(stateVector),
11048
+ compacted: toBase64(encrypted)
11049
+ })}`;
11050
+ this.sendStateless(payload);
11051
+ this.compactionInFlightTimeout = setTimeout(() => {
11052
+ this.compactionInFlight = false;
11053
+ this.compactionInFlightTimeout = null;
11054
+ }, 3e4);
11055
+ } catch (e) {
11056
+ this.compactionInFlight = false;
11057
+ throw e;
11058
+ }
11059
+ }
11060
+ _handleCompactedBroadcast(jsonStr) {
11061
+ let parsed = {};
11062
+ try {
11063
+ parsed = JSON.parse(jsonStr);
11064
+ } catch {}
11065
+ if (parsed.doc_id && parsed.doc_id !== this.configuration.name) return;
11066
+ if (this.compactionInFlightTimeout) {
11067
+ clearTimeout(this.compactionInFlightTimeout);
11068
+ this.compactionInFlightTimeout = null;
11069
+ }
11070
+ this.compactionInFlight = false;
11071
+ this.updatesSinceCompaction = 0;
11072
+ const event = {
11073
+ docId: parsed.doc_id ?? this.configuration.name,
11074
+ by: parsed.by
11075
+ };
11076
+ this.emit("compacted", event);
10962
11077
  }
10963
11078
  destroy() {
11079
+ if (this.destroyed) return;
11080
+ this.destroyed = true;
11081
+ if (this.compactionDebounceTimer) {
11082
+ clearTimeout(this.compactionDebounceTimer);
11083
+ this.compactionDebounceTimer = null;
11084
+ }
11085
+ if (this.compactionInFlightTimeout) {
11086
+ clearTimeout(this.compactionInFlightTimeout);
11087
+ this.compactionInFlightTimeout = null;
11088
+ }
10964
11089
  this.e2eStore?.destroy();
10965
11090
  this.e2eStore = null;
10966
11091
  super.destroy();